JSXGraph - 前端交互式几何库

2021-10-17 18:00:00

首先,这么好用的工具鲜为人知,在 Github 开源了 13 年仅收获 800 多个 Star,不禁让我想到“黄钟毁弃”一词。

现代前端技术日新月异,前端可视化工具库也是层出不穷,一个比一个优秀。但是论及交互式几何绘制谁表现得最佳,我觉得非 JSXGraph 莫属。如果是 d3、ECharts 这样享有盛名的可视化工具库,我大可不写这篇博客,因为用的人大多了,但是 JSXGraph 的资料比较有限,还是有谈一谈的必要。

简介

进入 JSXGraph 的官网,会看到明显的 DynamicInteractive 这样的词汇,那么 JSXGraph 的 Interactive 究竟能达到怎样的效果?官网首页给出了一个演示,我直接搬运过来了。

请大胆地尝试拖动 , , 三个点:

好家伙,这简直就是一个在线的 GeoGebra 呀。

特点

它的数学库支持:

文档

学习一个工具最好的地方莫过于它的官方文档,JSXGraph 也是如此。官网给出了最全的 API 以及 289个示例,没有比官网更良心的了,生怕用户学不会属于是。除了官网,有大佬编写了自己的教程 JSXGraph Book,比官网更适合入门。

示例

从官网和 JSXGraph Book 搬运了一些示例:

限制性三体问题

var brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-1.5,1.5,1.5,-1.5], axis:false, grid:false}),
    mu = 1.0/81.45, i,
    ode = function () {
        var I = [17.066, 0],
            x0 = [0.994, 0, 0, -2.0015851063790825],
            N = 10000,
            data, dataX, dataY, i,
            f = function(t, x) {
                var m  = 1.0/81.45,
                    D1 = Math.sqrt(Math.pow((x[0]+m)*(x[0]+m)+x[2]*x[2],3)),
                    D2 = Math.sqrt(Math.pow((x[0]-(1-m))*(x[0]-(1-m))+x[2]*x[2],3)),
                    y = [];
                y[0] = x[1];
                y[1] = x[0]+2*x[3]-(1-m)*(x[0]+m)/D1-m*(x[0]-(1-m))/D2;
                y[2] = x[3];
                y[3] = x[2]-2*x[1]-(1-m)*x[2]/D1-m*x[2]/D2;
                return y;
            };
        data = JXG.Math.Numerics.rungeKutta('rk4', x0, I, N, f);
        dataX = [];
        dataY = [];
        for(i in data) {
            dataX[i] = data[i][0];
            dataY[i] = data[i][2];
        }
        return [dataX, dataY];
    };
// earth
brd.create('point', [-mu, 0], {
    withLabel: false,
    strokeColor: 'none',
    fillColor: '#4096EE',
    size: 12,
    fixed: true
});
// moon
brd.create('point', [1-mu, 0], {
    withLabel: false,
    strokeColor: 'none',
    fillColor: 'gray',
    size: 3,
    fixed: true
});
// our space shuttle
var apolloPath = brd.createElement('curve', ode(), {
    strokeColor: 'red',
    strokeOpacity: 0.3,
    strokeWidth: 3,
    visible: true,
    needsRegularUpdate: false
});
apolloPath.hasPoint = function () {
    return false;
};
var apollo = brd.create('point', [1, 0], {
    withLabel: false,
    strokeColor: 'red',
    fillColor: 'red',
    size: 3,
    face: '<>',
    fixed: true
});
apollo.moveAlong(function (i) {
    return [apolloPath.dataX[i%apolloPath.dataX.length], apolloPath.dataY[i%apolloPath.dataY.length]];
}, 2000);
// a text in upper right corner to stop the animation
brd.create('text', [0.8, 1.3, '<div id="stop-animation">Stop Animation</div>'], {fontSize:8});
document.getElementById('stop-animation').addEventListener('click', function () {
    brd.stopAllAnimation();
});

Lissajous 曲线

参数方程:

var brd = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-12,12,12,-12], keepaspectratio:true});
var a = brd.create('slider', [[5,10],[9,10],[0,3,6]], {name:'a'});
var b = brd.create('slider', [[5,9],[9,9],[0,2,6]], {name:'b'});
var A = brd.create('slider', [[5,8],[9,8],[0,5,8]], {name:'A'});
var B = brd.create('slider', [[5,7],[9,7],[0,5,8]], {name:'B'});
var d = brd.create('slider', [[5,6],[9,6],[0,0,Math.PI]], {name:'&delta;'});
brd.create('curve', [
        function(t){ return A.Value()*Math.sin(a.Value()*t+d.Value()); },
        function(t){ return B.Value()*Math.sin(b.Value()*t); },
        0, 2*Math.PI], {strokeColor:'#aa2233', strokeWidth:2}
);

函数作图

<input type="text" value="sin(x)*x">
<input type="button" value="Plot" onClick="plot()">
<input type="button" value="Clear" onClick="clear()"> 
<input type="button" value="Add Tangent" onClick="addTangent()">
<input type="button" value="Add Derivative" onClick="addDerivative()">
var brd = initBoard();
var fcn, curve;
plot();
function initBoard()
{
    if (brd) {
        JXG.JSXGraph.freeBoard(brd);
    }
    return JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-5,8,8,-5], axis:true});
}
function plot()
{
    var txtfcn = document.getElementById('fcn-input').value;
    fcn = brd.jc.snippet(txtfcn, true, 'x', true);
    curve = brd.create('functiongraph', [fcn,xLim1,xLim2], {strokeWidth:2});
}
function clear()
{
    brd = initBoard();
    fcn = null;
    curve = null;
}
function addTangent()
{
    if (JXG.isFunction(fcn)) {
        var p = brd.create('glider', [1,0,curve], {name:'drag me'});
        brd.create('tangent', [p], {strokeWidth:1});
    }
}
function addDerivative()
{
    if (JXG.isFunction(fcn)) {
        brd.create('functiongraph', [JXG.Math.Numerics.D(fcn),xLim1,xLim2], {strokeWidth:1, dash:2});
    }
}
function xLim1()
{
    var c = new JXG.Coords(JXG.COORDS_BY_SCREEN, [0,0], brd);
    return c.usrCoords[1];
}
function xLim2()
{ 
    var c = new JXG.Coords(JXG.COORDS_BY_SCREEN, [brd.canvasWidth,0], brd);
    return c.usrCoords[1];
}

Riemann Sum

var brd = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-8,4,8,-4]});
var s = brd.create('slider', [[1,3.5],[5,3.5],[1,10,50]], {name:'n', snapWidth:1});
var a = brd.create('slider', [[1,2.5],[5,2.5],[-10,-3,0]], {name:'start'});
var b = brd.create('slider', [[1,1.5],[5,1.5],[0,2*Math.PI,10]], {name:'end'});
var f = function(x){ return Math.sin(x); };
var plot = brd.create('functiongraph', [f,function(){return a.Value();}, function(){return b.Value();}]);
var os = brd.create('riemannsum',[f,
    function(){ return s.Value(); }, function(){ return "left"; },
    function(){ return a.Value(); },
    function(){ return b.Value(); }
    ], {fillColor:'#ffff00', fillOpacity:0.3}
);
brd.create('text', [-6,-3,function(){ return 'Sum='+(JXG.Math.Numerics.riemannsum(f,s.Value(),'left',a.Value(),b.Value())).toFixed(4); }], {fontSize:12});

计算交点

var brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-5,3,5,-3], keepAspectRatio:true});
var A = brd.create('point', [-2,0], {name:"A"});
var B = brd.create('point', [-1,-1], {name:"B", visible:false});
var C = brd.create('point', [0,0], {name:"C"});
var circle1 = brd.create('circle', [A,B]);
var circle2 = brd.create('circle', [C,A]);
var inter1 = brd.create('intersection', [circle1,circle2,0], {name:'I_1'});
var inter2 = brd.create('intersection', [circle1,circle2,1], {name:'I_2'});
var line = brd.create('line', [inter1,inter2]);

Limacon

var brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-15,18,15,0], keepAspectRatio:true});
var p3 = brd.create('point', [0,4], {face:'x', size:2, name:"P_{3}", fixed:true});
var p4 = brd.create('point', [0,8], {face:'x', size:3, name:"P_{4}", fixed:true});
var c1 = brd.create('circle', [p4,p3]);
var p6 = brd.create('glider', [0,0,c1], {face:'o', size:3, name:"P_{6}"});
var g = brd.create('line', [p3,p6]);
var c2 = brd.create('circle', [p6,3]);
var p14_1 = brd.create('intersection', [c2,g,0], {size:2, name:"M", trace:true});
var p14_2 = brd.create('intersection', [c2,g,1], {size:2, name:"N", trace:true});

P 范数

var brd = JXG.JSXGraph.initBoard('jxgbox', {axis:true, boundingbox:[-5,3,5,-3], keepaspectratio:true});
var pNorm = brd.create('slider', [[0,2.5],[3,2.5],[0,3.5,10]], {name:'p'});
var m = brd.create('point',[0,0], {name:'m'});
brd.create('curve', [fcn, [
    function(){ return m.X(); },
    function(){ return m.Y(); }
    ], 0, Math.PI*2], {curveType:'polar', strokeColor:'#aa2233', strokeWidth:2}
);
function fcn(t)
{
    var p = pNorm.Value();
    return 2.0 / Math.pow( Math.pow(Math.abs(Math.cos(t)),p) + Math.pow(Math.abs(Math.sin(t)),p) , 1.0/p );
}

Newton 法


$f(x)$
<table width="600" border="0" cellpadding="0" cellspacing="0">
    <tr>
        <th>f(x)</th>
        <th>
            <input style="width:120px; border:none; padding:5px; margin-left:2px;" type="text" id="fcn-input" value="(x-2)*(x+1)*x*0.2" size="30"/>
            <input type="button" value="Set" onClick="newGraph()">
        </th>
    </tr>
    <script type="text/javascript">
        var STEP = 10;
        for (let i=0; i<STEP; i++)
        {
             document.write('<tr><td align="center">$x_{' + i + '}$</td><td align="right"><font id="xv' + i + '"></font></td></tr>');
        }
    </script>
</table>
var txtfcn = document.getElementById('fcn-input').value;
var x0 = 3; // 迭代初值
var brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-5,5,5,-5], axis:true});
var ax = brd.defaultAxes.x;
var g = brd.create('functiongraph', [txtfcn], {strokeWidth: 2});
var x = brd.create('glider', [x0,0,ax], {name:'x_{0}', color:'magenta', size:4});
newGraph();
Newton(x, STEP);
brd.on('update', xval);
xval();
function Newton(p, i)
{
    if (i>0) {
        var f = brd.create('glider', [
            function(){ return p.X(); },
            function(){ return g.Y(p.X()); },
            g], {name:'', style:3, color:'green'}
        );
        var l = brd.create('segment', [p,f], {strokeWidth:0.5, dash:1, strokeColor:'black'});
        var t = brd.create('tangent', [f], {strokeWidth:0.5, strokeColor:'#0080c0', dash:0});
        var x = brd.create('intersection', [ax,t,0], {name:'x_{' + (STEP-i+1) + '}', style:4, color:'red'});
        Newton(x, --i);
    }
}
function newGraph()
{
    txtfcn = document.getElementById('fcn-input').value;
    g.generateTerm('x', 'x', txtfcn);
    brd.update();
}
function xval()
{
    for (let i=0; i<STEP; i++)
    {
        document.getElementById('xv' + i).innerHTML = (brd.select('x_{' + i + '}').X()).toFixed(14);
    }
}

Lindenmayer 系统

<textarea id="input-code" rows=15 cols=35 wrap="off">
var level = 6;
var axiom = 'A';
var rules = {
    'A':'B-A-B',
    'B':'A+B+A',
    '+' : '+',
    '-' : '-'
};
var symbols = { 'A':'F', 
                'B':'F', 
                '+':'+', 
                '-':'-', 
                '[':'[',
                ']':']' 
              };
var angle = 60;
var len = 500/Math.pow(2,level);
turtle.setPos(-250*Math.pow(-1,level),-250);
turtle.rt(90*Math.pow(-1,level));
</textarea>
<input type="button" value="Run" onClick="run()">
<input type="button" value="Clear" onClick="clearTurtle()">
brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-500,300,500,-300], keepAspectRatio:true});
var turtle = brd.create('turtle');
var shrink = 1.0;
run();
function expander(level, axiom, rules)
{
    this.axiom = axiom;
    this.rules = rules;
    this.source = (level>1) ? new expander(level-1,axiom,rules) : (new function() {
        // Axiom:
        this.code = axiom;
        this.pos = 0;
        this.next = function() {
            if (this.pos>=this.code.length) {
                return null;
            }
            return this.code.charAt(this.pos++);
        }
    });
    this.code = '';
    this.pos = 0;
    this.next = function() {
        while (this.pos>=this.code.length) // produce new symbols from source
        {
            this.pos = 0;
            var pattern = this.source.next();
            if (!pattern) {
                return null; // Finished
            }
            this.code = this.rules[pattern];
        }
        return this.code.charAt(this.pos++);
    }
}
function plot(generator, symbols, len, angle, t, shrink)
{
    for (var c; c=generator.next(); c)
    {
        switch(symbols[c]) {
            case 'F':
                t.fd(len);
                break;
            case 'f':
                t.penUp();
                t.fd(len);
                t.penDown();
                break;
            case '+':
                t.lt(angle);
                break;
            case '-':
                t.rt(angle);
                break;
            case '[':
                t.pushTurtle();
                len *= shrink;
                break;
            case ']':
                t.popTurtle();
                len /= shrink;
                break;
            default:
                return;
        }
    }
}
function run()
{
    var code = document.getElementById('input-code').value;
    if (!code) {
        return;
    }
    turtle.cs();
    turtle.hideTurtle();
    eval(code);
    var generator = new expander(level, axiom, rules);
    plot(generator, symbols, len, angle, turtle, shrink);
}
function clearTurtle()
{
    turtle.cs();
}

Infinity

var brd = JXG.JSXGraph.initBoard('jxgbox-Infinity', {boundingbox: [-6,6,6,-6], keepAspectRatio:true, showCopyright:false});
var S = brd.create('slider', [[-5,-6],[3,-6],[0,0.95,1]], {name:'S'});
var hue = brd.create('slider', [[-5,-7],[3,-7],[0,8,36]], {name:'color'});
var points = [];
points[0] = brd.create('point',[5,5], {name:''});
points[1] = brd.create('point',[-5,5], {name:''});
points[2] = brd.create('point',[-5,-5], {name:''});
points[3] = brd.create('point',[5,-5], {name:''});
function quadrangle(pt, n)
{
    var col;
    var arr = new Array();
    for(var i = 0; i < 4; i++)
    {
        arr[i] = brd.create('point', 
            [function(t) {
                return function () {var x = pt[t].X(); var x1 = pt[(t+1)%4].X(); var s = S.Value(); return x+(x1-x)*s; }
            }(i),
            function(t) {
                return function () {var y = pt[t].Y(); var y1 = pt[(t+1)%4].Y(); var s = S.Value(); return y+(y1-y)*s; }
            }(i)
            ], {size:1, name:'', withLabel:false,visible:false});
    }
    col = function() { return JXG.hsv2rgb(hue.Value()*n,0.7,0.9); };
    brd.create('polygon', pt, {fillColor: col});
    if (n>0)
    {
        quadrangle(arr, --n);
    }
}
quadrangle(points,30);

Lagrange 插值

<input type="button" value="Add Point" onClick="addPoint()">
var brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-5,10,7,-6], axis:true});
var p = [];
p[0] = brd.create('point', [-1,2], {size:4});
p[1] = brd.create('point', [3,-1], {size:4});
var f = JXG.Math.Numerics.lagrangePolynomial(p);
var graph = brd.create('functiongraph', [f,-10,10], {strokeWidth:3});
var d1 = brd.create('functiongraph', [JXG.Math.Numerics.D(f), -10, 10], {dash:1});
var d2 = brd.create('functiongraph', [JXG.Math.Numerics.D(JXG.Math.Numerics.D(f)), -10, 10], {dash:2});
function addPoint()
{
    var point = brd.create('point', [(Math.random()-0.5)*10,(Math.random()-0.5)*3], {size:4});
    p.push(point);
    brd.update();
}

摆线

var brd = JXG.JSXGraph.initBoard('jxgbox-Rolling', {boundingbox:[-2,4,2,-2.5], axis:false, keepAspectRatio:true, showCopyright:false, showClearTraces:true, showNavigation:false});
var M = brd.create('point',[-2,0],{name:'M', face:'o', size:1, visible:false});
var N = brd.create('point',[2,0],{name:'N', face:'o', size:1, visible:false});
brd.create('line', [M,N], {color:'black'});
var r = brd.create('slider', [[-1,-1.5], [1,-1.5], [0,1,3]], {name:'r'});
var l = brd.create('slider', [[5,-1.5], [10,-1.5], [-2.,0,18.]], {name:'l'});
var C = brd.create('point', [function(){return l.Value()}, function(){return r.Value()}], {color:'blue', size:1, name:'C'});
var c = brd.create('circle', [C, function(){return r.Value()}], {color:'orange', fillOpacity:0.1});
var A0 = brd.create('point', [function(){return C.X()}, function(){return 2*r.Value()}], {visible:false});
var A = brd.create('point', [function(){return ((A0.X()-C.X())*Math.cos(l.Value()/r.Value())+(A0.Y()-C.Y())*Math.sin(l.Value()/r.Value()))+C.X()},
                             function(){return (-(A0.X()-C.X())*Math.sin(l.Value()/r.Value())+(A0.Y()-C.Y())*Math.cos(l.Value()/r.Value()))+C.Y()}],
                             {size:1, color:'red', trace:true});

调和共轭点

参考自 JSXGraph Book 3.5, , 三点共线,满足

的共线的 点,称为 相对 的调和共轭点。

var brd = JXG.JSXGraph.initBoard('jxgbox', {boundingbox:[-5,5,5,-5]});
var A = brd.create('point', [-4,-2]);
var B = brd.create('point', [0,-2]);
var a = brd.create('line', [A,B], {color:'green'});
var C = brd.create('glider', [4,0,a]);
var E = brd.create('point', [1,4], {name:'E', size:2, color:'blue'});
var b = brd.create('line', [A,E], {color:'green'});
var c = brd.create('line', [B,E], {color:'green'});
var F = brd.create('glider', [0,0,b], {name:'F', size:2, color:'blue'});
var d = brd.create('line', [C,F], {color:'green'});
var G = brd.create('intersection', [d,c,0], {name:'G', size:2, color:'blue'});
var e = brd.create('line', [A,G], {color:'grey', dash:"2"});
var f = brd.create('line', [B,F], {color:'grey', dash:"2"});
var H = brd.create('intersection', [e,f], {name:'H', size:2, color:'blue'});
var g = brd.create('line', [E,H], {color:'grey', dash:"2"});
var D = brd.create('intersection', [a,g,0]);

实现

首先,把下面的代码保存成index.html文件:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" type="text/css" href="http://jsxgraph.uni-bayreuth.de/distrib/jsxgraph.css"/>
        <script type="text/javascript" src="http://jsxgraph.uni-bayreuth.de/distrib/jsxgraphcore.js"></script>
    </head>
    <body>
        <div id="jxgbox" class="jxgbox" style="width:500px; height:500px;"></div>

        <!-- 把HTML代码放在下面(如果有HTML) -->

        <!-- 把javascript代码放在下面 -->
        <script type="text/javascript">

        </script>
    </body>
</html>

针对以上示例,分别把 HTML 和 javascript 代码复制到对应的位置。保存,用浏览器打开即可。

Author

青崖同学

Release

2021-10-17 18:00:00

License

Creative Commons